День 1 Летней школы Julia 2025
Введение в функции
Последняя глава нашего дня будет посвящена введению в функции.
В языке Julia функция представляет собой объект, который сопоставляет
кортеж значений аргументов с возвращаемым значением.
Именно поэтому мы сначала познакомились с кортежами.
Но подробно о связи кортежей и функций мы поговорим в конце второго и начале третьего дня.
А пока что обратим внимание на то, как задать функцию:
Для этого пропишем function, название нашей функции и в круглых скобках укажем параметры нашей функции.
Словом параметры мы будем называть переменные, которые перечислены в определении функции.
Они представляют собой имена, которые функция использует для ссылки на передаваемые ей значения.
Параметры функции служат как «заполнители» для значений, которые будут использованы в теле функции.
Аргументы — это конкретные значения или данные, передаваемые функции при её вызове.
Аргументы подставляются в места параметров при выполнении функции.
Аргументы могут быть константами, переменными, выражениями или даже результатами других функций.
В качестве аналогии если функция — это рецепт блюда, то параметры — это список ингредиентов, а аргументы — это сами ингредиенты, которые добавляют, следуя рецепту.
function панакота(основа, фрукты)
println(основа * фрукты)
end
панакота("🥛","🍓")
панакота("🥥","🍒")
Теперь обратим внимание на особенность функций в динамических языках программирования.
Давайте создадим функцию с параметром а, которая возвращает квадрат этого параметра.
function square_area(a)
return a^2
end
Ещё раз убедимся в том, что функции в julia это объекты и присвоим переменной sa значение в виде нашей функции.
sa = square_area
И вызвав sa(9) получим ожидаемый результат.
sa(9)
Чуть менее ожидаемым является результат вызова нашей функции, вычисляющей площадь
квадрата по стороне, в случае, когда мы передаём в неё строку.
Эта функция выполнилась и даже вывела результат.
Дело в том, что наша функция возвращает квадрат аргумента. А для строки возведение
в квадрат означает конкатенацию с самой собой, что мы и наблюдаем.
square_area("сторона")
Теперь создадим более сложную функцию. Укажем, что в качестве аргумента может передаваться только объект вещественного типа: целочисленное, рациональное, Float. Но не строка или комплексное число.
И у нас поменялось возвращаемое значение - вместого одного числа будет возвращаться именованный кортеж из периметра и площади.
И в этот раз мы не указали ключевое слово return, но для повышения читабельности это лучше делать.
function circle_params(r::Real)
(perim = 2π*r, area = π*r^2)
end
Вызвав функцию, мы можем сразу же обратиться к элементу кортежа и получить значение
circle_params(1).perim
Однако если же мы передадим в качестве аргумента строку, то будет выведена ошибка, так как параметром нашей функции является вещественное число.
circle_params("радиус")
Изменяемость и неизменяемость
Теперь обратим внимание на следующий код:
Зададим целочисленную переменную
a = 1
И функцию, внутри которой параметр меняет своё значение на 100.
function change_value(x)
x = 100
end
Запустим эту функцию и получим ожидаемый результат.
change_value(a)
Но вот что является неожиданным, так это то, что переменная а не поменяла своего значения.
a
Давайте снова посмотрим на нашу функцию.
function change_value!(x)
x = 100
end
В ней строка x = 100 означает то, что мы связываем со значением 100 переменную x. И выходит, что нам неважно какое число мы передавали в качестве аргумента.
Исправим эту ситуацию, указав x += 10.
Тогда действительно возвращаемое значение зависит от значения нашей переменной.
function change_value(x)
x += 10
end
a = 3
change_value(a)
Но переменная а так и осталась равной 3
Это связано с тем, что x = x+10 всё ещё является присваиванием переменной x значения суммы аргумента и 10. Для наглядности распишем это как y = x + 10. В первом случае x будет означать параметр. А во второй строке тела функции - x означает уже переменную.
x += 10 ~ x = x + 10
function change_value(x)
y = x + 10
x = y
end
Но как-то это всё странно. Что же это за язык такой, в котором функция не может поменять переменную, которую мы в неё передаём.
Надо ещё раз понять, что же обозначает x = 1 и x = 2
Не стоит воспринимать x как некоторую ячейку, которая хранит в себе значение, и что записав x = 2 мы меняем значение в этой ячейке.
Лучше это представлять как связь имени и значения. И когда мы пишем x = 2 это означает, что x разрывает связь с 1 и теперь обозначает (а не хранит) 2.
Мы можем называть нашего кота разными именами. Но от этого он не перестанет быть котом.
Поэтому, когда мы писали x = 100 внутри функции, мы просто взяли объект 100 и вернули его, называя его при этом x.
кошка = '🐈'
мышка = '🐈'
кот = '🐈'
print(кошка, мышка, кот)
Теперь же давайте посмотрим на функцию, которая меняет первый элемент некоторого контейнера.
function change_first_elem!(x)
x[1] = 100
end
Передадим в эту функцию вектор v=[1,2,3]
v = [1,2,3]
change_first_elem!(v)
v
В результате после вызова функции вектор v поменялся.
Но если бы мы сделали функцию такой:
function change_first_elem!(x)
x = [100, 2, 3]
end
v = [1, 2, 3]
change_first_elem!(v)
То всё будет так же как и в случае, когда мы передавали число. То есть сам аргумент не поменялся.
v
Но не все контейнеры обладают возможностью менять свои элементы.
Если мы передадим в нашу функцию кортеж, то произойдёт ошибка.
Ведь мы с вами уже знаем, что кортеж - неизменяемый тип данных.
А значит и строчка x[1] = 100 в нашей функции некорректна.
t = (1,2,3)
change_first_elem!(t)
t
Так же и единица является неизменяемым типом.
Теперь мы можем написать функцию, которая превратит единицу в 100.
Но есть нюанс.
В первом случае у нас было число, а в текущем варианте - вектор.
a = [1]
function change_value!(x)
x[1] = 100
end
change_value!(a)
a
Заметьте, что мы добавили восклицательный знак в конце названия функции.
Это принятое соглашения и хорошая практика использования функций, которые изменяют аргумент, передаваемый в функцию.
Работа с коллекциями
Теперь представим, что у нас есть задача посчитать синус для каждого элемента в коллекции и вернуть получившуюся коллекцию.
Если мы воспользуемся таким синтаксисом, то это будет означать синус от 4 аргументов. И такого метода нет.
sin(1,2,3,4)
Если мы передадим вектор или кортеж, то и для них тоже нет соответствующего метода.
sin([1,2,3,4])
sin((3,2,3,4))
Для выполнения нашей задачи подойдёт использование векторного варианта функции. В этом случае она применит функцию для каждого элемента и вернёт коллекцию того же типа. То есть для кортежа вернёт кортеж, а для вектора - вектор.
sin.((1,2,3,4,5)) # нетипичный сценарий
sin.([1,2,3,4,5]) # типичный сценарий
Способы задать функцию
Помимо привычного для многих языков программирования способа задания функции.
function line(k::Real,b::Real,x::Real)::Real
return k*x + b
end
Существует и сокращённая версия, больше похожая на функцию в математике.
parabola(a,b,c,x) = a*x^2 + b*x + c
parabola(1,2,3,4)
Анонимные функции
map(f,collection)
И, наконец, перейдём к последней части нашего дня.
Давайте познакомимся с такой удобной функцией как map.
Она принимают в качестве первого параметра функцию, которая будет обрабатывать данные из коллекций, хранящихся в оставшихся параметрах.
Выполнив её, мы получили вектор корней из нашего исходного вектора.
На первый взгляд, она выполняет то же, что и векторная запись функции (который мы использовали для синуса в прошлой главе).
map(√,[1,4,9,16])
Но на между ними есть различие. Тот синтаксис с точкой, который мы использовали, на самом деле тоже сокращение для функции
sqrt.([1,4,9,16])
то же, что и
broadcast(√,[1,4,9,16])
И давйте посмотрим, как map можно применить для создания рациональных чисел.
Она будет брать числители из первого массива, а знаменатели из второго. При этом отметим, что коллекции у нас разной длины. И map останавливает выполнение операций, когда одна из коллекций заканчивается.
map(//,[1,2,3,4],[5,7,9])
В этом и заключается отличие map от broadcast
map(+,1,[1,2,3])
broadcast(+,1,[1,2,3])
Мы рассмотрели работу функцию map для того, чтобы познакомиться с понятием анонимных функций.
Вместо того, чтобы создавать отдельную функцию, а потом передавать её в качестве первого аргумента, мы создадим как бы временную функцию, которая не будет засорять наше пространство имён.
для этого используется синтаксис x -> x^2 + 1
t = map(x -> x^2 + 1, (1,2,3))
Заметьте, что мы использовали кортеж и вернулся кортеж, по которому мы можем получить 3 элемент, например.
t[3]
Также анонимные функции могут содержать два параметра.
map((x,y) -> x^2 + y, [4,5,6], [1,2,3])
Кроме этого, анонимные функции могут быть полезны в функции foreach.
В отличие от map она также выполняет все операции, но не возвращает коллекцию.
foreach((x,y) -> x^2 + y, [4,5,6], [1,2,3])
Но если мы пропишем println(), то мы увидим, как работает наша функция.
foreach((x,y) -> println(x^2 + y), [4,5,6], [1,2,3]) == nothing
На этом лекционный материал первого дня подошёл к концу, но мы с большим интерресом ждём ваши вопросы, а также предлагаем вам пройти